/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.websocket;

import org.apache.catalina.util.Conversions;
import org.apache.coyote.http11.upgrade.UpgradeProcessor;
import org.apache.tomcat.util.buf.Utf8Decoder;
import org.apache.tomcat.util.res.StringManager;

import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CoderResult;

/**
 * Represents a complete WebSocket frame with the exception of the payload for
 * non-control frames.
 *
 * @deprecated Replaced by the JSR356 WebSocket 1.1 implementation and will be
 * removed in Tomcat 8.0.x.
 */
@Deprecated
public class WsFrame {

	private static final StringManager sm =
			StringManager.getManager(Constants.Package);

	private final boolean fin;
	private final int rsv;
	private final byte opCode;
	private final byte[] mask = new byte[4];
	private final ByteBuffer payload;
	private long payloadLength;

	/**
	 * Create the new WebSocket frame, reading data from the processor as
	 * necessary.
	 *
	 * @param first     First byte of data for this frame
	 * @param processor Processor associated with the WebSocket connection on
	 *                  which the frame has been sent
	 * @throws IOException If a problem occurs processing the frame. Any
	 *                     exception will trigger the closing of the WebSocket
	 *                     connection.
	 */
	private WsFrame(byte first,
	                UpgradeProcessor<?> processor) throws IOException {

		int b = first & 0xFF;
		fin = (b & 0x80) > 0;
		rsv = (b & 0x70) >>> 4;
		opCode = (byte) (b & 0x0F);

		b = blockingRead(processor);
		// Client data must be masked
		if ((b & 0x80) == 0) {
			throw new IOException(sm.getString("frame.notMasked"));
		}

		payloadLength = b & 0x7F;
		if (payloadLength == 126) {
			byte[] extended = new byte[2];
			blockingRead(processor, extended);
			payloadLength = Conversions.byteArrayToLong(extended);
		} else if (payloadLength == 127) {
			byte[] extended = new byte[8];
			blockingRead(processor, extended);
			payloadLength = Conversions.byteArrayToLong(extended);
		}

		if (isControl()) {
			if (payloadLength > 125) {
				throw new IOException();
			}
			if (!fin) {
				throw new IOException();
			}
		}

		blockingRead(processor, mask);

		if (isControl()) {
			// Note: Payload limited to <= 125 bytes by test above
			payload = ByteBuffer.allocate((int) payloadLength);
			blockingRead(processor, payload);

			if (opCode == Constants.OPCODE_CLOSE && payloadLength > 2) {
				// Check close payload - if present - is valid UTF-8
				CharBuffer cb = CharBuffer.allocate((int) payloadLength);
				Utf8Decoder decoder = new Utf8Decoder();
				payload.position(2);
				CoderResult cr = decoder.decode(payload, cb, true);
				payload.position(0);
				if (cr.isError()) {
					throw new IOException(sm.getString("frame.invalidUtf8"));
				}
			}
		} else {
			payload = null;
		}
	}

	/**
	 * Read the next WebSocket frame, reading data from the processor as
	 * necessary.
	 *
	 * @param processor Processor associated with the WebSocket connection on
	 *                  which the frame has been sent
	 * @param block     Should this method block until a frame is presented if no
	 *                  data is currently available to process. Note that is a
	 *                  single byte is available, this method will block until the
	 *                  complete frame (excluding payload for non-control frames) is
	 *                  available.
	 * @throws IOException If a problem occurs processing the frame. Any
	 *                     exception will trigger the closing of the WebSocket
	 *                     connection.
	 */
	public static WsFrame nextFrame(UpgradeProcessor<?> processor,
	                                boolean block) throws IOException {

		byte[] first = new byte[1];
		int read = processor.read(block, first, 0, 1);
		if (read == 1) {
			return new WsFrame(first[0], processor);
		} else if (read == 0) {
			return null;
		} else if (read == -1) {
			throw new EOFException(sm.getString("frame.readEos"));
		} else {
			throw new IOException(
					sm.getString("frame.readFailed", Integer.valueOf(read)));
		}
	}

	public boolean getFin() {
		return fin;
	}

	public int getRsv() {
		return rsv;
	}

	public byte getOpCode() {
		return opCode;
	}

	public boolean isControl() {
		return (opCode & 0x08) > 0;
	}

	public byte[] getMask() {
		return mask;
	}

	public long getPayLoadLength() {
		return payloadLength;
	}

	public ByteBuffer getPayLoad() {
		return payload;
	}

	/*
	 * Blocks until a aingle byte has been read
	 */
	private int blockingRead(UpgradeProcessor<?> processor)
			throws IOException {
		int result = processor.read();
		if (result == -1) {
			throw new IOException(sm.getString("frame.eos"));
		}
		return result;
	}

	/*
	 * Blocks until the byte array has been filled.
	 */
	private void blockingRead(UpgradeProcessor<?> processor, byte[] bytes)
			throws IOException {
		int read = 0;
		int last = 0;
		while (read < bytes.length) {
			last = processor.read(true, bytes, read, bytes.length - read);
			if (last == -1) {
				throw new IOException(sm.getString("frame.eos"));
			}
			read += last;
		}
	}

	/*
	 * Intended to read whole payload and blocks until it has. Therefore able to
	 * unmask the payload data.
	 */
	private void blockingRead(UpgradeProcessor<?> processor, ByteBuffer bb)
			throws IOException {
		int last = 0;
		while (bb.hasRemaining()) {
			last = processor.read();
			if (last == -1) {
				throw new IOException(sm.getString("frame.eos"));
			}
			bb.put((byte) (last ^ mask[bb.position() % 4]));
		}
		bb.flip();
	}
}
